![]() |
|
|||||
Die Art des Schedulings kann nicht in Java spezifiziert werden. Java-Prioritäten werden auf Betriebssystems-Prioritäten abgebildet, wobei diese Abbildung nicht 1:1 und keineswegs eindeutig ist. Nach welchen Heuristiken der Scheduler Threads dann noch Zeitscheiben zuordnet, ist nicht vorhersehbar. Nicht deterministisches Scheduling Der Ausführungsplan muss also als nicht deterministisch angesehen werden. Wann und wie lange welcher Thread zur Ausführung kommt, hängt vom Scheduler und dem jeweiligen Zustand des Systems ab.
Eine Methode, z.B. service(), kann dabei durchaus von zwei oder mehreren Threads für dasselbe Objekt obj gleichzeitig ausgeführt werden (siehe Abb. 9.3). Dies macht Laufzeittests auf Fehlerfreiheit mühsam, da sich aufgrund der Testläufe nicht schlüssig ermitteln lässt, wie die nächste Ausführung beim Kunden aussieht. 9.1.3 Synchronisation
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Jede Art des Wartens aufeinander nennt man Synchronisation, das Gegenteil ist asynchrones Verhalten. |
Wird eine Methode aufgerufen, ohne auf ihr Operationsende zu warten, ist dies ein asynchroner Aufruf, ansonsten ein synchroner. Wartet ein Thread auf die Beendigung eines anderen, nennt man sie sychronisiert.
Locking: Sperren von Objekten bzw. Klassen
Java synchronisiert grundsätzlich mit Hilfe von Locks (Sperren) auf Objekte oder auf Klassen (einem Lock auf Class-Objekte). Jedes Objekt bzw. jede Klasse (Class-Objekt) hat genau ein Lock.
Wird irgendeine Klassen- oder Instanz-Methode mit dem Schlüsselwort synchronized markiert, so muss sich ein Thread für die Ausführung dieser Methoden erst einmal das zugehörige Objekt- bzw. Klassen-Lock holen.
|
Besitz des Locks: exklusive Ausführungsrechte
Während ein Thread (Thread 1 in Abb. 9.4) ein Lock besitzt, kann kein anderer dieses erwerben und hat somit keinerlei Ausführungsrechte für alle synchronisierten Methoden dieses Objekts bzw. dieser Klasse.
Der Thread, der das Lock für eine synchronisierte Methode erworben hat, kann nicht nur die eine, sondern alle synchronisierten Methoden des Objekts bzw. der Klasse exklusiv ausführen.
Nachdem ein Thread die letzte synchronisierte Methode eines Objekts bzw. einer Klasse beendet hat, wird das entsprechende Lock wieder freigegeben und kann von einem anderen Thread erworben werden (Thread 2 in Abb. 9.4).
Deadlock: Gegenseitige Blockade von Threads
Mit Einführung der Synchronisation besteht die Gefahr, dass zwei Threads gegenseitig aufeinander warten oder - schlimmer - alle aufeinander warten, d.h., das gesamte System »friert ein«.
Diese Situation ist aus dem Straßenverkehr bekannt, wo an einer Kreuzung vier Fahrzeuge aus den verschiedenen Richtungen gleichzeitig ankommen und aufgrund der Rechts-vor-Links-Regel alle aufeinander warten müssen. Dieser Deadlock wird dann durch einen mutigen Fahrer bzw. einen Unfall beseitigt.
Zwei Alternativen zum Starten eines Threads
Startpunkt eines Threads ist immer ein Objekt der Klasse Thread, wobei es allerdings zwei Alternativen gibt (Abb. 9.5).
|
zu A1: Dies ist eher die Ausnahme. Will man wirklich nur einen Thread erschaffen, dann ist diese Alternative ok und auch einfacher (Abb. 9.6).
zu A2: Dies ist der Normalfall. Man will Objekte konkreter Klassen wie z.B. einen Druck-Manager, einen Video-Player, eine Kalkulation oder Transaktion erschaffen, welche ihren Service asynchron ausführen sollen.
Service als Thread oder asynchroner Dienst
Dies bedeutet aber, dass man eine von der Thread-Klasse unabhängige Klasse bzw. Klassen-Hierarchie entwerfen muss, mit der zusätzlichen Eigenschaft, asynchron ausgeführt werden zu können (Abb. 9.6).
|
main() und run():
Start- und Endpunkt der Ausführung
Die Methode run() ist äquivalent zur Methode main() einer Applikation. Die Methode main() bildet den Start- bzw. Endpunkt des Hauptthread, run() den Start- und Endpunkt eines zusätzlichen Threads. Thread wie Applikation sind mit dem Ende von run() bzw. main() beendet.
Ist run() beendet, sagt man auch, dass der Thread den Endzustand tot (dead) erreicht hat, kurz tot ist.
Es sind folgende vier Regeln zur Ausführung wichtig:
|
|
Nachfolgend zwei einfache Muster, um einen Thread zu erzeugen:
// 1. Muster
class SubThread extends Thread { public void run() { ... } // siehe 1. Regel }
// 2. Muster
class ClassWithThread implements Runnable { public void run() { ... } // siehe 1. Regel }
// Testklasse zur Anlage und Start der Threads
public class TestThread { public static void main(String[] args) { Thread st1= new SubThread(); Thread st2 = new Thread(new ClassWithThread());
st1.start(); // Methode start() ruft run() auf st2.start(); // run() läuft asynchron zu main() } }
Dieses Testbeispiel zeigt noch ein interessantes Detail:
| Eine Java-App2 läuft so lange, bis der letzte Nicht-Dämon-Thread3 beendet ist. |
Im Testbeispiel ist zwar main(), d.h. der Hauptthread, sofort nach Aufruf der beiden Methoden start() beendet, aber die Java-App ist erst beendet, wenn die beiden anderen Threads beendet sind.
Würde man in main() die Methode st1.run() anstatt st1.start() ausführen, wäre dies eine synchrone Ausführung im Hauptthread (siehe 4. Regel).
Bei synchroner Ausführung muss zuerst st1.run() beendet werden, bevor die nächste Anweisung st2.start() ausgeführt wird.
Um die Methoden, die auf Threads operieren, zu verstehen und sinnvoll zu benutzen, muss man die möglichen Zustände kennen, in denen sich ein Thread befinden kann (Abb. 9.7).
|
Nach dem Start des Threads wird der Thread aktiv und wechselt aufgrund des Scheduling zwischen lauffähig und code-ausführend (»läuft«).
Ist run() beendet oder hat man die unsichere deprecated Methode stop() aufgerufen, so ist der Thread beendet, kurz tot.
Daneben gibt es noch vier Zustände, in die ein Thread absichtlich oder unbeabsichtigt geraten kann, und die man auch generell mit dem Begriff suspendierend bezeichnen kann. Es ist aber wichtig, sie aufgrund ihres Typs zu unterscheiden.
Der Zustand »schlafend« wird nur durch die statische Thread-Methode sleep(long ms) ausgelöst und lässt den ausführenden Thread für die angegebene Zeit in Millisekunden ruhen. Danach geht er wieder in den aktiven Zustand über.
Zustand: blockiert vs. nicht blockiert bei Input/Output
Ist ein Thread mit Daten-Ein-/Ausgabe (I/O) beschäftigt, muss er eventuell aufgrund nicht bereiter I/O-Geräte warten. Dies ist eigentlich sogar der Normalfall, da in der Regel Peripherie-Geräte wesentlich langsamer in der Ausführung sind als die CPU.
Ein Thread, der auf I/O wartet, wird von der JVM suspendiert bzw. blockiert, damit andere aktive Threads ausgeführt werden können. Der Thread wird erst wieder aktiv, wenn die Daten bereitstehen oder geschrieben wurden. Diese Art der Ein-/Ausgabe ist also synchron.
»Friert« ein I/O-Gerät ein, bedeutet dies, dass der zugehörige Thread nicht mehr läuft, was auch eine Art von Tod darstellt.
Das Gegenteil ist »nicht blockierend bei I/O« (nonblocking I/O). Hier wartet der Thread nicht. Liegen Daten von der Eingabe bereit, liest er sie oder er kehrt sofort wieder zurück. Kann der Thread bei der Ausgabe Daten nicht schreiben, kehrt er ebenfalls sofort zurück.
| Java unterstützt - mit wenigen Ausnahmen - nur synchrone I/O, d.h., es gibt keine direkte Unterstützung für eine nicht blockierende I/O. |
Der Mechanismus der Synchronisation wurde bereits in 9.1.3 beschrieben. An dem Zustands-Diagramm in 9.7 erkennt man schon die Gefahr des Deadlocks. Wartet ein Thread auf den Eintritt in einen synchronisierten Code-Bereich und bekommt das Lock nicht, ist er so gut wie tot.
Bei Threads, die auf denselben Objekten operieren, gibt es zwei Möglichkeiten der Kontrolle.
Entweder prüfen die Threads, ob der Zustand der Objekte passend ist, oder umgekehrt, die Objekte kontrollieren aufgrund ihrer Zustände die Threads. Bei Java hat man sich für die zweite Möglichkeit entschieden.
Monitor: Die Objekte kontrollieren die Threads
| Ein Objekt, das die auf ihm operierenden Threads suspendieren und wieder aktivieren kann, heisst Monitor. |
| Jedes Objekt, das synchronisierten Code enthält, ist ein Monitor. |
Synchronisation mittels wait() und notify()
Zur Kontrolle der Threads hat die Klasse Object und somit jedes Objekt die Methoden wait() und notify() bzw. notifyAll().
Innerhalb einer synchronisierten Methode versetzt wait() einen Thread in den Zustand »wartend«.
Ist ein passender Zustand erreicht, kann das Objekt eine oder alle wartenden Threads mittels notify() bzw. notifyAll() in der gleichen oder einer anderen synchronisierten Methode wieder aktivieren. Dieser Aufruf kann natürlich nur aus einem anderen aktiven Thread heraus erfolgen (siehe Abb. 9.8).
|
Jede Instanz hat einen eigenen Zustand »wartend«, in den alle Threads, die wait() auf diese Instanz ausführen, überführt werden (siehe Abb. 9.8, Thread 1).

notify(): Verlassen des Wartezustands
Kurz zu den Regeln, verbunden mit notify():
| Wird in derselben Instanz ein notify() von einem noch aktiven Thread ausgeführt und mehr als ein Thread wartet, wird ein zufälliger Thread in den aktiven Zustand überführt. |
| Es gibt bei notify() keine Reihenfolge à la FIFO (First-In First-Out5 ). |
| Die Methode notifyAll() aktiviert alle zur Instanz wartenden Threads. |
Wer zuerst wartet, wird also bei notify() bzw. notifyAll() nicht unbedingt zuerst wieder aktiviert. Die Auswahl eines Threads ist willkürlich (in Abb. 9.8 gibt es allerdings nur einen wartenden Thread 1).
Für statische Methoden läuft der oben beschriebene Mechanismus analog, nur eben auf Klassen-Ebene, d.h., der Monitor ist das Objekt Class.
Wie in Abb. 9.7 zu sehen, können die suspendierenden Zustände »schlafend« oder »wartend« unterbrochen werden. Dazu muss zu diesem Thread die Instanz-Methode interrupt() von einem anderen laufenden Thread aufgerufen werden.
interrupt(): Unterbrechung der Zustände wartend und schlafend
Die Methode interrupt() bewirkt bei einem
| suspendierten Thread einen Übergang nach »aktiv« und die Auslösung einer InterruptedException, die - da keine Runtime-Exception - abgefangen werden muss. |
| aktiven Thread, dass ein Interrupt-Flag gesetzt wird, das entweder während der Ausführung abgefragt werden kann oder beim nächsten suspendierenden Zustand dann die Ausnahme auslösen sollte (Näheres siehe 9.13). |
InterruptedIOException, im Prinzip ja, aber ...
Korrekterweise müsste in Abb. 9.7 auch beim Verlassen des Zustands »blockiert« ein »unterbrochen« eingetragen werden, denn auf eine Unterbrechung sollten I/O-Methoden mit einer InterruptedIOException reagieren.
Das ist aber bei den meisten IO-Methoden nicht implementiert, selbst System.in von Sun reagiert darauf nicht. Deshalb wurde dieser Guard weggelassen. Hier helfen nur Abbrüche durch Time-outs.
Die Klasse Thread enthält als zentraler Repräsentant weitere wichtige Methoden, die kurz vorgestellt werden sollen.
Es gibt drei bzw. vier Konstuktoren für die beiden Alternativen der Thread-Erzeugung (siehe 9.2). Für die zweite Alternative muss bei der Anlage der Thread-Instanz ein target-Objekt übergeben werden, das run() enthält. Jedem Thread kann optional ein Name gegeben und eine Thread-Gruppe zugeordnet werden.
| Thread-Gruppen sind dazu da, Threads nach ihrer Funktionalität zu gruppieren, um dann alle Threads einer Gruppe gleichzeitig manipulieren zu können. |
Nachfolgend die sieben Konstruktoren (target teilweise optional):
public Thread( [Runnable target] ); public Thread( [Runnable target,] String name); public Thread(ThreadGroup group, Runnable target); public Thread(ThreadGroup group, [Runnable target,] String name);
Statische Methoden der Klasse Thread
Es folgen einige wichtige statische Methoden:
| static int activeCount(): Ermittelt die Anzahl der zur Zeit aktiven Threads der aktuellen Thread-Gruppe. |
| static Thread currentThread(): Ermittelt den zur Zeit laufenden (code-ausführenden) Thread. |
| static boolean interrupted(): Ein Thread kann einen anderen unterbrechen (siehe 9.3.8). Bei einem aktiven Thread wird das »unterbrochen«-Flag mit interrupted() getestet und anschließend wieder auf false gesetzt (siehe Alternative isInterrupted() in 9.4.4). |
| static void sleep(long ms) throws InterruptedException: Der laufende Thread wird für die angegebene Zeit in Millisekunden in den Zustand »schlafend« versetzt. |
| static void yield(): Überlässt einem anderen lauffähigen Thread die Möglichkeit der Ausführung (ist abhängig vom Scheduler!).6 |
Prioritäten: MIN_PRIORITY .. MAX_PRIORITY
Jeder Thread hat eine Priorität (siehe 9.1.2). Java kennt Prioritäten zwischen static final int MIN_PRIORITY (=1) und MAX_PRIORITY (=10).
Die vorgegebene Default-Priorität ist NORM_PRIORITY (=5), womit z.B. der Hauptthread, gestartet durch main(), beginnt.
| Java-Prioritäten können nur im Idealfall 1:1 auf Betriebssystems-Prioritäten abgebildet werden.7 |
In welchem Maße der Scheduler Threads mit höherer Priorität bevorzugt, ist nicht festgelegt. Es liegt zwischen den Extrema »nur Threads mit der höchsten Priorität sind laufberechtigt« und »alle Threads, egal welcher Priorität, sind gleich laufberechtigt«.
Die Priorität eines Threads kann zur Laufzeit gelesen und gesetzt werden (siehe 9.4.4).
Für die Prioriät beim Start gilt folgende Regel:
Default-Priorität eines Threads
| Ein neuer Thread übernimmt bei der Anlage die Prioriät des Threads, aus dem er erschaffen wurde. |
Instanz-Methoden der Klasse Thread
Von den insgesamt 20 nicht deprecated Instanz-Methoden werden hier nur die wichtigsten, noch nicht besprochenen vorgestellt.
| public final int getPriority(), public final void setPriority(int newPriority): Liest oder setzt die Priorität dieses Threads. |
| public final ThreadGroup getThreadGroup(): Liest die Thread-Gruppe, zu der dieser Thread gehört. |
| public void interrupt(): Löst für diesen Thread eine Unterbrechung aus (Details siehe 9.3.8). |
| public boolean isInterrupted(): Testet für diesen Thread, ob eine Unterbrechung vorliegt, ohne das »unterbrochen«-Flag zu verändern (siehe Alternative interrupted()). |
| public final native boolean isAlive(): Liefert true, wenn dieser Thread gestartet wurde, aber noch nicht tot ist, ansonsten false. |
| public final void join( [long ms] ) throws InterruptedException: Blockiert den Thread, der join() ausführt, so lange bis der Thread, verbunden mit der Instanz, stirbt bzw. tot ist. Bei einer Zeitangabe ist der Thread maximal ms Millisekunden blockiert (siehe auch Abb. 9.7). |
Dämon-Threads:
ohne eigene Existenzberechtigung
| public final void setDaemon(boolean on): Setzt den Thread als Dämon-Thread (im Sinne von Hintergrunds-/Dienst-Thread). Diese Anweisung hat unbedingt vor dem Thread-Start zu erfolgen. |
Nach dem Überblick über Konzepte und Instrumente soll ein erstes kleines Beispiel den Einsatz und die Wirkung einiger der genannten Methoden demonstrieren.
Zuerst werden zwei Threads nach den beiden in 9.2 beschriebenen Alternativen angelegt und dann diverse statische und nicht statische Methoden der Thread-Klasse getestet:
// Hilfs-Klasse class TestRun { // der aktuelle Thread maximal n Mal für
// sleepTime Millisec. schlafen lassen
static void run(int sleepTime, int n ) { try { while (n-->0) { System.out.println(Thread.currentThread().getName()); Thread.sleep(sleepTime); } } catch (InterruptedException e) { System.out.println("Interrupted: "+Thread.currentThread()); } } }
Subklasse von Thread: Overriding run()
class SubThread extends Thread { public void run() { TestRun.run(1000,3); // dreimal 1 sec schlafen } }
class ClassWithThread implements Runnable { public void run() { TestRun.run(1000,3); // dreimal 1 sec schlafen } }
public class Test { public static void main(String[] args) { Thread st0= new SubThread(); Thread st1= new Thread(new ClassWithThread()); // st1.setDaemon(true); ¨
System.out.println(Thread.currentThread().getThreadGroup()); System.out.println(Thread.currentThread().getPriority());
st0.start(); st1.start(); st0.interrupt(); } }
java.lang.ThreadGroup[name=main,maxpri=10] 5 Thread-1 Thread-0 Interrupted: Thread[Thread-0,5,main] Thread-1 Thread-1 |
Erklärung: Das erste println() gibt mit Hilfe von toString() die Standard-Thread-Gruppe aus, zu der der Hauptthread gehört. Threads ohne Namen werden automatisch mit Null beginnend numeriert. Danach folgt die Prioritäts-Angabe des Hauptthreads.
Obwohl Thread-0 vor Thread-1 gestartet wurde, entscheidet allein der Scheduler über die CPU-Zuteilung, d.h., wer sich auf der Konsole zuerst meldet, ist Sache des Schedulers.
Thread-0 wird im Schlaf-Zustand unterbrochen, was eine InterruptedException auslöst (siehe Konsol-Meldung). Thread-1 durchläuft dagegen dreimal die Schleife, was zu den letzten beiden Ausgaben führt.
Nur noch
Dämon-Threads: Programmende
zu ¨: Wird die auskommentierte Anweisung ausgeführt, ist Thread-1 ein Dämon. Das Programm ist somit beendet, wenn Hauptthread sowie Thread-0 terminieren, d.h., Thread-1 wird sich nur einmal melden.
Wenn Threads parallel auf dieselben Objekte zugreifen können, wird die Frage interessant, in welcher Reihenfolge Lese- und Schreib-Operationen auf den Objekten ablaufen.
Atomare Operation: nicht unterbrechbar
Unter einer atomaren Operation versteht man eine Operation, die entweder als Ganzes oder überhaupt nicht durchgeführt wird. Beginnt die Operation, kann sie nicht mehr unterbrochen werden, sie ist unteilbar. Das Gegenteil ist nicht atomar.
Betrachten wir ein einfaches Objekt Int:
Primitive Typen == atomare Operationen ?
class Int { private int i; boolean test(int j) { i= j; return i==j;
} }
In einer single-threaded Applikation ist das Ergebnis der Methode test() trivial, nämlich true.
Bei Multi-Threading lautet die Frage, ob test() atomar ist. Die Methode ist dann atomar, wenn Setzen auf j und Vergleich mit j nicht unterbrechbar sind, also ganz oder gar nicht ablaufen.
Nachfolgend ein einfacher Test von Int mit Konsol-Ausgabe:
Testprogramm
zu atomaren Operationen
class AtomicTest implements Runnable { Int iobj;
AtomicTest(Int iobj) { this.iobj= iobj; }
public void run() { int i= 0; // maximal 100000 mal auf true testen while (iobj.test(i) && ++i <100000); System.out.println(Thread.currentThread().getName()+": "+i); } }
In der Klasse Test wird die Methode test() von genau einer Int-Instanz parallel von vier Threads ausgeführt:
Ein Objekt, vier schreibende Threads
public class Test { public static void main(String[] args) { Int iobj= new Int(); // nur ein Objekt Thread[] st= new Thread[4];
Lesen und Schreiben des
primitiven Typs int ist nicht atomar
for (int i=0; i<st.length;i++) { st[i]= new Thread(new AtomicTest(iobj)); // st[i].setPriority(Thread.MAX_PRIORITY-2*i); ¨ st[i].start(); } } }
Thread-2: 100000 Thread-0: 100000 Thread-1: 100000 Thread-3: 100000 |
Thread-0: 15 Thread-1: 100000 Thread-2: 100000 Thread-3: 100000 |
Thread-1: 0 Thread-3: 0 Thread-0: 7 Thread-2: 100000 |
Erklärung: Die Methode test() in der Klasse Int ist nicht atomar, vor dem Vergleich von i mit j kann ein anderer Thread bereits den Wert von i geändert haben, was allerdings recht selten vorkommt.
Mit unterschiedlichen Prioritäten kann man versuchen, Einfluss auf den CPU-Zuteilungs-Algorithmus des Schedulers zu nehmen.
Wird die auskommentierte Anweisung ¨ in Test ausgeführt, so erhalten die Threads abnehmende Prioritäten.
Bei preemptive Scheduling sollte sich dann Thread-0 zuerst melden, danach Thread-1, Thread-2 und Thread-3. Dies ist allerdings wieder vom Scheduler abhängig.
Im Zusammenhang mit Thread-Problemen sind folgende Begriffe wichtig:
| Ein Code-Abschnitt (Methode oder Block) heißt reentrant, wenn er von mehreren Threads gleichzeitig ausgeführt wird. |
Im letzten Beispiel war die Methode test() reentrant. Ein Thread führt i=j; aus, während ein anderer den Vergleich i==j; durchführt.
Race-Condition: Thread-Wettrennen
| Unter Race-Condition versteht man Fehler bzw. Probleme, die durch reentrant Code entstehen, d.h., Ergebnisse werden von der zufälligen Reihenfolge parallel ablaufender Operationen abhängig. |
In Verbindung mit Reentrance gilt folgende Regel:
| Nur Lese- oder Schreib-Operationen auf Variablen bis vier Byte Länge sind atomar. Alle anderen Operationen sind nicht atomar. |

Vermeidlich atomare
Operationen
Somit sind die meisten einzelnen Anweisungen nicht atomar und können ergo zu Race-Conditions führen. Dies ist sicherlich nicht sehr glücklich. Man betrachte die beiden »unverdächtigen« Anweisungen zu einer int i bzw. long l:
i++; // muss gelesen und geschrieben werden return l; // 8 Byte benötigen zwei Lese-Operationen
Dies sind zwei überraschende Beispiele für nicht atomare Anweisungen.
Thread-sicher:
»no problems« bei Multi-Threading
| Code bzw. Methoden heißen thread-sicher (thread-safe), wenn es zu keinen Problemen in einer Multi-Threading-Umgebung kommt. |
Können Race-Conditions auftreten, ist der Code nicht thread-sicher. Leider ist dies nicht hinreichend, will sagen, nicht das einzige Problem.
Thread-Sicherheit erfordert in einer komplexen Ablaufumgebung umfangreiche Maßnahmen, von denen im Weiteren nur wenige besprochen werden können.8
Guarded
Concurrency: Single-Thread-Ausführung durch Synchronisation
Mit Hilfe des Schlüsselworts synchronized werden Methoden oder Blocks vor Reentrance geschützt (guarded):
| Der mit synchronized geschützte Code kann nicht gleichzeitig von mehreren Threads ausgeführt werden. |
Werden in einem synchronisierten Code-Block keine suspendierenden Anweisungen ausgeführt und gibt es nur Operationen auf primitiven Datentypen, ist der Code-Block sogar logisch atomar, d.h., zuerst müssen alle Anweisungen von einem Thread durchlaufen werden, bevor ein anderer denselben Code-Block ausführt.
Die Methode test() der Klasse Int in 9.5.1 kann also ganz einfach atomar und in diesem Fall dann auch thread-sicher gemacht werden:
Synchronisierte primitive
Operationen: thread-sicher und logisch atomar
class Int { private int i; synchronized boolean test(int j) { // guarded concurrency i=j; return i==j; } }
Bevor einer der vier Threads die Methode test() in dem Beispiel in 9.5.1 ausführen darf, muss er vom Monitor - der Instanz iobj der Klasse Int - das Lock erhalten. Hat ein Thread das Lock, gehen die anderen bei Aufruf von test() in den Zustand »warten auf Lock« über (siehe Abb. 9.7).
| Methoden-Ebene erfolgen: |
synchronized [Modifiers] ResultType method (pList) ...Dann muss der ausführende Thread bei einer Instanz-Methode zuerst das Lock des zugehörigen Objekts this, bei einer statischen Methode das Lock des Class-Objekts erwerben.
| Block-Ebene erfolgen: |
synchronized(ReferenzExpression) { synchronizedBlock }Dann muss der ausführende Thread zuerst das Lock des Objekts erwerben, auf das ReferenzExpression zeigt.
Es gelten folgende drei Regeln:
Beschränkungen für synchronized
| 1. | Konstruktoren können nicht synchronized werden. |
| 2. | In Interfaces ist synchronized nicht erlaubt. |
| 3. | Methoden können synchronized oder nicht überschrieben werden. |
Konstruktoren brauchen nur synchronisiert werden, sollten sie während der Initialisierung eine Referenz des unfertigen Objekts this an andere Objekte herausreichen. In diesem Fall muss dann eben Block-Sychronisation verwendet werden.
Block-Synchronisation ist feiner
| Die Sychronisation auf Block-Ebene ist feiner und kann die der Methoden emulieren (siehe nachfolgendes Beispiel). |
In der Klasse Sync werden verschiedene äquivalente Synchronisations-Mechanismen demonstriert.
Die statischen Methoden sf1(), sfV1() und sfV2() sowie die Instanz-Methoden f1() und f2() sind äquivalent.
class Sync {
Synchronisation von statischen Methoden
// Statisch: Methoden- vs. Block-Synchronisation
public synchronized static void sf1() { //.. } public static void sfV1() { try { // Einsatz von Reflexion
synchronized(Class.forName("Sync")) { //.. } } catch (ClassNotFoundException e) { } } public static void sfV2() { synchronized(new Sync().getClass()) { //.. } }
Synchronisation von Instanz-Methoden
// Instanz: Methoden- vs. Block-Synchronisation
public synchronized void f1() { //.. } public void f2() { synchronized(this) { //.. } } }
Vorteile der Methoden-
Synchronisation
| Methoden-Synchronisation ist sofort im Methoden-Kopf zu erkennen, Block-Synchronisation dagegen ohne Zugriff auf den Code nicht. |
Sofern als Lock andere Objekte als die eigene Instanz oder Klasse verwendet werden, sind die Auswirkungen der Synchronisation schwer abzuschätzen. Wird dies vom Anwender nicht durchschaut, kann es schnell zu Deadlocks kommen (siehe 9.6.3).
Ein Thread, der den Lock besitzt, kann als einziger beliebige synchronisierte Methoden bzw. Blöcke desselben Objekts ausführen.9
Erst wenn ein Thread die letzte synchronisierte Methode des Objekts verlassen hat, kann ein anderer Thread den Lock erlangen.
Voll- vs. Teil-
Synchronisation
Sind - bis auf die Konstruktoren - alle Methoden eines Objekts synchronisiert und alle Felder private erklärt, so heißt eine Objekt vollsynchronisiert, ansonsten teilsynchronisiert.
| vollsynchronisiert, so kann nur ein Thread zu einem Zeitpunkt auf dem Objekt operieren. |
| teilsynchronisiert, so können neben dem Thread, der das Lock besitzt, beliebig viele andere Threads unsynchronisierte Methoden parallel im Objekt ausführen. |
Bei einem Deadlock sind zumindest zwei Threads im Zustand »warten auf Lock«, das jeweils einen anderen Thread im selben Zustand hält. Diese Threads blockieren sich gegenseitig und können nicht mehr aktiv werden.
Gibt es keinen aktiven Thread mehr, friert eine Java-App ein.